package com.googlecode.derby.customize;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;

import com.googlecode.derby.customize.annotation.Column;
import com.googlecode.derby.customize.annotation.Table;
import com.googlecode.derby.customize.exception.SqlMakerException;

/**
 * 取得・登録・更新・削除などSQL文の簡易動作のみを定義する抽象クラス.
 *
 * @author o.tanaka
 * @version 2009/12/11
 */
public abstract class SqlMaker {

    private final Map<String, AutoSql> tablez;
    private AutoSql autoSql;
    private Object bindData;
    private List<BindData> whereBindDataz = new ArrayList<BindData>();
    private final Map<String, Integer> selectBindCountMap =
        new WeakHashMap<String, Integer>();
    private final Map<String, Integer> insertBindCountMap =
        new WeakHashMap<String, Integer>();
    private final Map<String, Integer> updateBindCountMap =
        new WeakHashMap<String, Integer>();
    private final Map<String, Integer> deleteBindCountMap =
        new WeakHashMap<String, Integer>();
    private final Map<String, Integer> callableBindCountMap =
        new WeakHashMap<String, Integer>();
    private int updateColumnCount;
    private List<String> tableNames;

    /**
     * 初期化.
     */
    protected SqlMaker() {
        this.tablez = new WeakHashMap<String, AutoSql>();
        this.autoSql = new AutoSql();
    }

    /**
     * @param aOrderbyColumn
     *            order by column
     */
    public void addOrderby(final String aOrderbyColumn) {
        final StringBuilder orderby = new StringBuilder();
        if (StringUtils.isNotEmpty(this.autoSql.getOrderby())) {
            orderby.append(this.autoSql.getOrderby());
            orderby.append(", " + aOrderbyColumn);
            this.autoSql.setOrderby(orderby.toString());
        } else {
            orderby.append("ORDER BY " + aOrderbyColumn);
            this.autoSql.setOrderby(orderby.toString());
        }
    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            class Field
     * @param aObject
     *            data class
     * @return sql
     */
    abstract String addWhere(
            String aTableName, List<ColumnData> aList, Object aObject);

    /**
     * @param aSql
     *            sql
     * @param aObjects
     *            data class
     */
    public void addWhere(final String aSql, final Object[] aObjects) {
        String where = this.autoSql.getWhere();
        if (StringUtils.isNotEmpty(where)) {
            where += " AND ";
        } else {
            where = "WHERE ";
        }
        where += aSql;
        int index = this.whereBindDataz.size();
        for (final Object object : aObjects) {
            final Method[] methodz = object.getClass().getMethods();
            index = this.addWhereColumSet(index, object, methodz);
        }
        this.autoSql.setWhere(where);
    }

    private int addWhereColumSet(
            final int index,
            final Object object,
            final Method[] methodz) {
        int i = index;
        try {
            for (final Method method : methodz) {
                String columnName = null;
                String type = null;
                final Column column = method.getAnnotation(Column.class);
                Object value = null;
                if (column != null) {
                    columnName = column.value()[0];
                    type = column.value()[1];
                    value = method.invoke(object);
                }
                if (value != null) {
                    i++;
                    final BindData bind = new BindData();
                    bind.setIndex(Integer.valueOf(i));
                    bind.setColumnName(columnName);
                    bind.setType(type);
                    bind.setValue(value);
                    this.whereBindDataz.add(bind);
                }
            }
        } catch (final IllegalArgumentException e) {
            throw new SqlMakerException(
                    "addWhereColumSet error:",
                    this, e);
        } catch (final IllegalAccessException e) {
            throw new SqlMakerException(
                    "addWhereColumSet error:",
                    this, e);
        } catch (final InvocationTargetException e) {
            throw new SqlMakerException(
                    "addWhereColumSet error:",
                    this, e);
        }
        return i;
    }

    /**
     * tableNames 初期化
     */
    public void clearTableNames() {
        this.tableNames = new ArrayList<String>();
    }

    private AutoSql createSql(
            final String tableName,
            final Class<?> aClass) {
        final List<ColumnData> list = this.setColumnData(aClass);
        this.getTable(tableName);
        if (this.autoSql == null) {
            this.autoSql = new AutoSql();
        }
        this.autoSql.setColumnData(list);
        this.autoSql.setSelect(this.select(tableName));
        this.autoSql.setCreateTable(
                this.createTable(
                tableName, list));
        return this.autoSql;
    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            class Field
     * @return sql
     */
    abstract String createTable(
            final String aTableName,
            final List<ColumnData> aList);

    /**
     * @param aTableName
     *            table name
     * @param aClazz
     *            data class
     * @return AutoSql
     */
    public AutoSql delete(final String aTableName, final Class<?> aClazz) {
        this.autoSql = this.getTable(aTableName);
        if (this.autoSql == null) {
            this.autoSql = new AutoSql();
        }
        List<ColumnData> list = this.autoSql.getColumnData();
        if (list == null) {
            list = this.setColumnData(aClazz);
            this.autoSql.setColumnData(list);
        }
        this.autoSql.setDelete(this.delete(aTableName, list));
        return this.autoSql;
    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            data class
     * @return sql
     */
    public abstract String delete(
            final String aTableName,
            final List<ColumnData> aList);

    /**
     * @return autoSql
     */
    protected final AutoSql getAutoSql() {
        return this.autoSql;
    }

    /**
     * @return bindData
     */
    protected final Object getBindData() {
        return this.bindData;
    }

    /**
     * @return callableBindCountMap
     */
    public final Map<String, Integer> getCallableBindCountMap() {
        return this.callableBindCountMap;
    }

    /**
     * @return deleteBindCountMap
     */
    public final Map<String, Integer> getDeleteBindCountMap() {
        return this.deleteBindCountMap;
    }

    /**
     * @return insertBindCountMap
     */
    public final Map<String, Integer> getInsertBindCountMap() {
        return this.insertBindCountMap;
    }

    /**
     * @return query
     */
    public final String getQuery() {
        return this.autoSql.getQuery();
    }

    /**
     * @return selectBindCountMap
     */
    public final Map<String, Integer> getSelectBindCountMap() {
        return this.selectBindCountMap;
    }

    /**
     * Table情報取得.
     * 
     * @return セットされたテーブルを取得
     */
    final AutoSql getTable(final String aKey) {
        return this.tablez.get(aKey);
    }

    /**
     * @return updateBindCountMap
     */
    public final Map<String, Integer> getUpdateBindCountMap() {
        return this.updateBindCountMap;
    }

    /**
     * @return updateColumnCount
     */
    public final int getUpdateColumnCount() {
        return this.updateColumnCount;
    }

    /**
     * @return whereBindDataz
     */
    public final List<BindData> getWhereBindDataz() {
        return this.whereBindDataz;
    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            class Field
     * @return sql
     */
    abstract String insert(String aTableName, List<ColumnData> aList);

    /**
     * @param aTableName
     *            table name
     * @param aClass
     *            data class
     * @return AutoSql
     */
    public AutoSql insertUpdate(
            final String aTableName,
            final Class<?> aClass) {
        this.autoSql = this.getTable(aTableName);
        if (this.autoSql == null) {
            this.autoSql = new AutoSql();
        }
        List<ColumnData> list = this.autoSql.getColumnData();
        if (list == null) {
            list = this.setColumnData(aClass);
            this.autoSql.setColumnData(list);
        }
        this.autoSql.setQuery(this.selectPk(aTableName, list));
        this.autoSql.setInsert(this.insert(aTableName, list));
        this.autoSql.setUpdate(this.update(aTableName, list));
        this.autoSql.setDelete(this.delete(aTableName, list));

        return this.autoSql;
    }

    /**
     * @param aTableName
     *            table name
     * @return "SELECT * FROM " + aTableName
     */
    String select(final String aTableName) {
        return "SELECT * FROM " + aTableName;
    }

    /**
     * @param aTableName
     *            table name
     * @param aObject
     *            data class
     * @return AutoSql
     */
    public AutoSql selectAutoWhere(
            final String aTableName,
            final Object aObject) {
        this.autoSql = this.getTable(aTableName);
        if (this.autoSql == null) {
            this.autoSql = new AutoSql();
        }
        List<ColumnData> list = this.autoSql.getColumnData();
        if (list == null) {
            list = this.setColumnData(aObject.getClass());
            this.autoSql.setColumnData(list);
        }
        this.bindData = aObject;
        this.selectBindCountMap.clear();
        this.autoSql.setWhere("WHERE "
            + this.addWhere(aTableName, list, aObject));

        return this.autoSql;
    }

    /**
     *
     */
    public void selectJoin() {
        // TODO SELECT文生成　sqlMaker.getAutoSql().setSelectへ
        // select ATABLE.*, BTABLE.* FROM ATABLE ATABLE, BTABLE BTABLE
        // tablenameが２件以上

    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            class Field
     * @return sql
     */
    abstract String selectPk(String aTableName, List<ColumnData> aList);

    /**
     * @param aAutoSql the autoSql to set
     */
    protected final void setAutoSql(final AutoSql aAutoSql) {
        this.autoSql = aAutoSql;
    }

    /**
     * @param aBindData the bindData to set
     */
    protected final void setBindData(final Object aBindData) {
        this.bindData = aBindData;
    }

    private List<ColumnData> setColumnData(final Class<?> aClass) {
        final Method[] methodz = aClass.getMethods();
        final List<ColumnData> columnz = new ArrayList<ColumnData>();
        for (final Method method : methodz) {
            final ColumnData columnData = new ColumnData();
            columnData.setMethodName(method.getName());
            final Column column = method.getAnnotation(Column.class);
            if (column != null) {
                if ((column.value().length == 5)) {
                    columnData.setColumnName(column.value()[0]);
                    columnData.setType(column.value()[1]);
                    columnData.setLength(column.value()[2]);
                    columnData.setNotNull(column.value()[3]);
                    columnData.setPk(column.value()[4]);
                } else if ((column.value().length == 4)) {
                    columnData.setColumnName(column.value()[0]);
                    columnData.setType(column.value()[1]);
                    columnData.setLength(column.value()[2]);
                    columnData.setNotNull(column.value()[3]);
                } else if ((column.value().length == 3)) {
                    columnData.setColumnName(column.value()[0]);
                    columnData.setType(column.value()[1]);
                    columnData.setLength(column.value()[2]);
                }
                columnz.add(columnData);
            }
        }
        return columnz;
    }

    /**
     * @param aQuery
     *            query
     */
    public final void setQuery(final String aQuery) {
        this.autoSql.setQuery(aQuery);
    }

    /**
     * @param aUpdateColumnCount the updateColumnCount to set
     */
    public final void setUpdateColumnCount(final int aUpdateColumnCount) {
        this.updateColumnCount = aUpdateColumnCount;
    }

    /**
     * @param aWhereBindDataz the whereBindDataz to set
     */
    public final void setWhereBindDataz(final List<BindData> aWhereBindDataz) {
        this.whereBindDataz = aWhereBindDataz;
    }

    /**
     * @param aClass
     *            table class
     */
    public void table(final Class<?> aClass) {
        final Table table = aClass.getAnnotation(Table.class);
        String tableName = null;
        if (table.value().length >= 1) {
            tableName = table.value()[0];
            if (this.tableNames != null) {
                this.tableNames.add(tableName);
            }
        }
        this.autoSql = this.tablez.get(tableName);
        if (this.autoSql == null) {
            this.autoSql = this.createSql(tableName, aClass);
            this.tablez.put(tableName, this.autoSql);
        }
    }

    /**
     * @return String
     * @see ToStringBuilder#reflectionToString(Object)
     */
    @Override
    public final String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    /**
     * @param aTableName
     *            table name
     * @param aList
     *            class Field
     * @return sql
     */
    abstract String update(String aTableName, List<ColumnData> aList);

}
